import { NextRequest, NextResponse } from 'next/server'
import { createServerSupabaseClient } from '@/lib/supabase'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const supabase = createServerSupabaseClient()
const bookId = params.id
// Get current user session
const authHeader = request.headers.get('authorization')
let user
try {
if (authHeader) {
const { data: { user: authUser }, error: authError } = await supabase.auth.getUser(authHeader.replace('Bearer ', ''))
if (!authError) user = authUser
} else {
const { data: { user: sessionUser }, error: sessionError } = await supabase.auth.getUser()
if (!sessionError) user = sessionUser
}
} catch (e) {
// Ignore auth errors
}
if (!user) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
)
}
// Get GitHub integration
const { data: profile } = await supabase
.from('profiles')
.select('github_integrations')
.eq('id', user.id)
.single()
const integration = profile?.github_integrations?.[bookId]
if (!integration) {
return NextResponse.json(
{ error: 'GitHub integration not found' },
{ status: 404 }
)
}
// Get files from GitHub repository
const owner = integration.github_username
const repo = integration.repository_name
const accessToken = integration.access_token
try {
// First, get the latest commit SHA to ensure we're comparing against the latest version
const latestCommit = await getLatestCommit(owner, repo, accessToken)
if (!latestCommit) {
console.log('๐ GitHub Compare API: No commits found - repository is empty')
return NextResponse.json({
committedFiles: {},
repositoryEmpty: true
})
}
console.log('๐ GitHub Compare API: Latest commit:', latestCommit.sha)
// Get repository contents using the tree API for better performance
const committedFiles = await getRepositoryFilesFromTree(owner, repo, accessToken, latestCommit.sha)
console.log('๐ GitHub Compare API: Found committed files:', Object.keys(committedFiles).length)
console.log('๐ GitHub Compare API: File paths:', Object.keys(committedFiles))
return NextResponse.json({
committedFiles,
latestCommitSha: latestCommit.sha,
repositoryEmpty: false
})
} catch (githubError) {
console.error('โ GitHub Compare API: Error fetching files:', githubError)
// Check if it's a 409 error (empty repository)
if (githubError instanceof Error && githubError.message.includes('409')) {
console.log('๐ GitHub Compare API: Repository is empty')
return NextResponse.json({
committedFiles: {},
repositoryEmpty: true
})
}
return NextResponse.json(
{ error: 'Failed to fetch files from GitHub', details: githubError instanceof Error ? githubError.message : String(githubError) },
{ status: 500 }
)
}
} catch (error) {
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
// Helper function to get the latest commit
async function getLatestCommit(owner: string, repo: string, accessToken: string): Promise<{ sha: string; commit: any } | null> {
try {
const url = `https://api.github.com/repos/${owner}/${repo}/commits/HEAD`
console.log(`๐ GitHub Compare API: Fetching latest commit from ${url}`)
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json'
}
})
if (!response.ok) {
if (response.status === 409) {
// Repository is empty
return null
}
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
}
return await response.json()
} catch (error) {
console.error('โ GitHub Compare API: Error fetching latest commit:', error)
throw error
}
}
// More efficient function using GitHub's tree API
async function getRepositoryFilesFromTree(owner: string, repo: string, accessToken: string, commitSha: string): Promise<{ [path: string]: string }> {
const files: { [path: string]: string } = {}
try {
// Get the tree for the commit (recursive to get all files)
const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${commitSha}?recursive=1`
console.log(`๐ GitHub Compare API: Fetching tree from ${treeUrl}`)
const treeResponse = await fetch(treeUrl, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json'
}
})
if (!treeResponse.ok) {
throw new Error(`GitHub Tree API error: ${treeResponse.status} ${treeResponse.statusText}`)
}
const treeData = await treeResponse.json()
console.log(`๐ GitHub Compare API: Found ${treeData.tree.length} items in tree`)
// Filter for files only (not directories) and exclude README.md
const fileItems = treeData.tree.filter((item: any) =>
item.type === 'blob' &&
item.path !== 'README.md' &&
!item.path.startsWith('.git')
)
console.log(`๐ GitHub Compare API: Processing ${fileItems.length} files`)
// Fetch file contents in batches to avoid rate limiting
const batchSize = 10
for (let i = 0; i < fileItems.length; i += batchSize) {
const batch = fileItems.slice(i, i + batchSize)
await Promise.all(batch.map(async (item: any) => {
try {
// Use the blob API to get file content by SHA
const blobUrl = `https://api.github.com/repos/${owner}/${repo}/git/blobs/${item.sha}`
const blobResponse = await fetch(blobUrl, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json'
}
})
if (blobResponse.ok) {
const blobData = await blobResponse.json()
// GitHub returns content as base64 if it's binary or large
let content = ''
if (blobData.encoding === 'base64') {
try {
content = Buffer.from(blobData.content, 'base64').toString('utf-8')
} catch {
// Skip binary files
console.log(`๐ GitHub Compare API: Skipping binary file ${item.path}`)
return
}
} else {
content = blobData.content
}
// Normalize file path to match BookWiz format
const normalizedPath = normalizeFilePath(item.path)
files[normalizedPath] = content
console.log(`๐ GitHub Compare API: Retrieved content for ${normalizedPath} (${content.length} chars)`)
} else {
console.error(`โ GitHub Compare API: Failed to fetch blob for ${item.path}:`, blobResponse.status)
}
} catch (fileError) {
console.error(`โ GitHub Compare API: Error fetching file ${item.path}:`, fileError)
}
}))
}
} catch (error) {
console.error(`โ GitHub Compare API: Error in getRepositoryFilesFromTree:`, error)
throw error
}
return files
}
// Helper function to normalize file paths to match BookWiz format
function normalizeFilePath(githubPath: string): string {
// Remove any leading slashes and normalize the path
const normalized = githubPath.replace(/^\/+/, '')
// If the file is directly in the root, just return the filename
// This matches how BookWiz stores file paths
const parts = normalized.split('/')
if (parts.length === 1) {
return parts[0]
}
// For nested files, return the full path
return normalized
}
// Keep the old function as fallback if needed
async function getRepositoryFiles(owner: string, repo: string, accessToken: string, path: string = ''): Promise<{ [path: string]: string }> {
const files: { [path: string]: string } = {}
try {
const url = `https://api.github.com/repos/${owner}/${repo}/contents${path ? `/${path}` : ''}`
console.log(`๐ GitHub Compare API: Fetching ${url}`)
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/vnd.github.v3+json'
}
})
if (!response.ok) {
if (response.status === 404) {
// Repository might be empty or path doesn't exist
console.log(`๐ GitHub Compare API: Path not found (404): ${path || 'root'}`)
return files
}
console.error(`โ GitHub Compare API: GitHub API error for ${url}:`, response.status, response.statusText)
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
}
const contents = await response.json()
// Handle single file vs array of files
const items = Array.isArray(contents) ? contents : [contents]
console.log(`๐ GitHub Compare API: Found ${items.length} items in ${path || 'root'}`)
for (const item of items) {
if (item.type === 'file' && item.name !== 'README.md') {
try {
// Get file content
const fileResponse = await fetch(item.download_url, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
})
if (fileResponse.ok) {
const content = await fileResponse.text()
const filePath = path ? `${path}/${item.name}` : item.name
files[filePath] = content
console.log(`๐ GitHub Compare API: Retrieved content for ${filePath} (${content.length} chars)`)
} else {
console.error(`โ GitHub Compare API: Failed to fetch content for ${item.name}:`, fileResponse.status)
}
} catch (fileError) {
console.error(`โ GitHub Compare API: Error fetching file ${item.name}:`, fileError)
}
} else if (item.type === 'dir') {
// Recursively get directory contents
const dirPath = path ? `${path}/${item.name}` : item.name
console.log(`๐ GitHub Compare API: Entering directory ${dirPath}`)
const dirFiles = await getRepositoryFiles(owner, repo, accessToken, dirPath)
Object.assign(files, dirFiles)
}
}
} catch (error) {
console.error(`โ GitHub Compare API: Error in getRepositoryFiles for path ${path}:`, error)
throw error // Re-throw instead of silently handling
}
return files
}